package org.forum.jstun;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Arrays;

/**
 * Classic Stun (RFC3489)
 */
public class StunClient implements Runnable {
	public interface SendListener {
		public StunMessage onSend(StunMessage msg);
	}

	public interface StunParser {
		public StunMessage parse(DatagramPacket packet);
	}

	private String mServerHost;
	private InetAddress mServerAddress;
	private int mServerPort;
	private String mUsername;
	private String mPassword;
	private int mPeriodicInterval = 1000*28;

	private boolean mActive = true;
	private DatagramSocket mSocket;
	private int mReceiveTimeout = 1000*2;
	private SendListener mSender = null;
	private StunParser mParser = null;

	public StunClient(String server, int port) {
		mServerHost = server;
		mServerPort = port;
	}

	public void setShareSecret(String username, String password) {
		mUsername = username;
		mPassword = password;
	}

	public void setTimeout(int receiveMillis, int periodicMillis) {
		mReceiveTimeout = receiveMillis;
		mPeriodicInterval = periodicMillis;
		try {
			if (mSocket!=null) mSocket.setSoTimeout(mReceiveTimeout);
		} catch (Exception e){}
	}

	public void setSendListener(SendListener listener) {
		mSender = listener;
	}

	public void setStunParser(StunParser parser) {
		mParser = parser;
	}

	@Override
	public void run() {
		start();
	}

	public void start() {
		try {
			mServerAddress = InetAddress.getByName(mServerHost);
			mSocket = new DatagramSocket();
			mSocket.setSoTimeout(mReceiveTimeout);
			System.out.println("connect to server "+mServerAddress.getHostAddress()+":"+mServerPort);
			mSocket.connect(mServerAddress, mServerPort);
		} catch (Exception e) {
			e.printStackTrace();
			stop();
			return;
		}
		
		mActive = true;
		boolean binding = true;
		StunMessage request = null;
		DatagramPacket packet = null;
		// stun over UDP without stream index, internet MTU recommend it is 576,
		// udp data length max is 548 (IP 20, UDP header 8).
		DatagramPacket receive = new DatagramPacket(new byte[1024], 1024);
		long current = System.currentTimeMillis();
		while(mActive) {
			if (binding) {
				// send binding request
				if (request==null) {
					request = buildMessage(StunMessage.TYPE_BINDING_REQUEST);
					packet = buildPacket(request);
				}
				sendPacket(mSocket, packet);
			}
			if (receivePacket(receive, mSocket)) {
				// receive packet
				if (binding) { 
					// check stun response
					StunMessage header = StunMessage.parseHeader(receive.getData());
					if (header!=null && StunMessage.transactionIdEquals(
						request.getTransactionId(), header.getTransactionId()) 
						&& mServerAddress.equals(receive.getAddress())
						&& mServerPort==receive.getPort()) {
						binding = false;
						request = null;
					}
				}
				StunMessage response = parseMessage(receive);
				analyseMessage(response);
			}
			if (System.currentTimeMillis()-current>mPeriodicInterval) {
				binding = true;
				current = System.currentTimeMillis();
			}
		}
	}

	public void stop() {
		mActive = false;
		if (mSocket!=null) mSocket.close();
		mSocket = null;
	}

	public boolean isActive() {
		return mActive;
	}

	/** Stun Message Attribute table - RFC3478
	An M indicates that inclusion of the attribute in the message is
	mandatory, O means its optional, C means it's conditional based on
	some other aspect of the message, and N/A means that the attribute is
	not applicable to that message type.

										  Binding  Shared  Shared  Shared
						Binding  Binding  Error    Secret  Secret  Secret
	Att.                Req.     Resp.    Resp.    Req.    Resp.   Error
																	Resp.
	_____________________________________________________________________
	MAPPED-ADDRESS      N/A      M        N/A      N/A     N/A     N/A
	RESPONSE-ADDRESS    O        N/A      N/A      N/A     N/A     N/A
	CHANGE-REQUEST      O        N/A      N/A      N/A     N/A     N/A
	SOURCE-ADDRESS      N/A      M        N/A      N/A     N/A     N/A
	CHANGED-ADDRESS     N/A      M        N/A      N/A     N/A     N/A
	USERNAME            O        N/A      N/A      N/A     M       N/A
	PASSWORD            N/A      N/A      N/A      N/A     M       N/A
	MESSAGE-INTEGRITY   O        O        N/A      N/A     N/A     N/A
	ERROR-CODE          N/A      N/A      M        N/A     N/A     M
	UNKNOWN-ATTRIBUTES  N/A      N/A      C        N/A     N/A     C
	REFLECTED-FROM      N/A      C        N/A      N/A     N/A     N/A

	 */
	private StunMessage buildMessage(short msgType) {
		StunMessage msg = new StunMessage(msgType);
		msg.setTransactionId(StunMessage.generateId(false));
		if (msgType == StunMessage.TYPE_BINDING_REQUEST) {
			if (mUsername!=null) msg.appendAttribute(new StunMessage.UsernamePassword(
					StunAttribute.TYPE_USERNAME, mUsername));
		}
		return msg;
	}

	private DatagramPacket buildPacket(StunMessage msg) {
		if (mSender!=null) msg = mSender.onSend(msg);
		if (msg==null || msg.isEmpty()) return null;
		if (msg.getAttribute(StunAttribute.TYPE_MESSAGE_INTEGRITY)==null && 
				mPassword!=null && !mPassword.isEmpty())
			msg.appendAttribute(StunMessage.generateMessageIntegrity(
				msg.getBytes(), mPassword.getBytes()));
		byte[] data = msg.getBytes();
		DatagramPacket packet = new DatagramPacket(data, data.length);
		// already connect to stun server
		return packet;
	}

	private boolean sendPacket(DatagramSocket socket, DatagramPacket packet) {
		boolean ret = false;
		try {
			socket.send(packet);
			ret = true;
		} catch (Exception e) {}
		return ret;
	}

	private boolean receivePacket(DatagramPacket packet, DatagramSocket socket) {
		boolean ret = false;
		try {
			// already set socket SoTimeout
			Arrays.fill(packet.getData(), (byte)0);
			socket.receive(packet);
			ret = true;
		} catch (Exception e) {}
		return ret;
	}

	private StunMessage parseMessage(DatagramPacket packet) {
		StunMessage receive = null;
		if (mParser!=null) receive = mParser.parse(packet);
		else receive = StunMessage.parse(packet.getData(), packet.getLength());
		return receive;
	}

	private void analyseMessage(StunMessage msg) {

	}

}